﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ImapX;
using System.Diagnostics;
using System.ComponentModel;
using GMigrate.Properties;
using ImapX.Parsing.Commands;
using ImapX.Parsing.DataItems;
using System.Text.RegularExpressions;

namespace GMigrate
{
    public struct MessageLocation
    {
        public MessageLocation(string folderPath, int uid)
        {
            this.folderPath = folderPath;
            this.uid = uid;
        }

        public string folderPath;
        public int uid;
    }
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            worker.WorkerReportsProgress = true;
            worker.DoWork += new DoWorkEventHandler(worker_DoWork);
            worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
            worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);

            if (Settings.Default.srcPassword != "")
            {
                srcPassword.Password = Password.Decrypt(Settings.Default.srcPassword);
            }
            if (Settings.Default.destPassword != "")
            {
                destPassword.Password = Password.Decrypt(Settings.Default.destPassword);
            }

            statusListBox.ItemsSource = StatusMessages;

            srcClient._client.TraceID = "S";
            destClient._client.TraceID = "D";
        }

        private ImapClient srcClient = new ImapClient("imap.gmail.com", 993, true);
        private ImapClient destClient = new ImapClient("imap.gmail.com", 993, true);

        public int numUploaded = 0;
        public int numSkipped = 0;
        public int numCopied = 0;
        public int numFixed = 0;
        public int numSkippedOnError = 0;

        private Message currentMessage;
        private int messageIndex;
        private int messageCount;
        private Folder currentFolder;

        // Messages that already exist on the server (so we can copy instead of storing the new message in a new mailbox)
        private Dictionary<string, MessageLocation> destMessages = new Dictionary<string, MessageLocation>();

        private List<Message> errorSrcMessages = new List<Message>();

        BackgroundWorker worker = new BackgroundWorker();

        private void btnRun_Click(object sender, RoutedEventArgs e)
        {
            btnRun.IsEnabled = false;
            btnRun.Content = "Running...";
            StatusMessages.Clear();

            worker.RunWorkerAsync();
        }

        public ObservableCollection<string> StatusMessages = new ObservableCollection<string>();

        private void Error(string message)
        {
            UI(() =>
            {
                StatusMessages.Add(message);
            });
        }

        public void UI(Action action)
        {
            Dispatcher.Invoke(action);
        }

        private void UpdateMessageDisplay()
        {
            UI(() =>
            {
                try
                {
                    skippedLabel.Content = numSkipped.ToString();
                    copiedLabel.Content = numCopied.ToString();
                    uploadedLabel.Content = numUploaded.ToString();
                    fixedLabel.Content = numFixed.ToString();
                    errorsLabel.Content = numSkippedOnError.ToString();

                    messagesProgressBar.Value = messageIndex;
                    messagesLabel.Content = String.Format("Messages: {0}/{1} - '{2} '", messageIndex, messageCount, currentMessage.Subject);
                }
                catch (Exception ex)
                {
                    Error("Error updating message status: " + ex.Message);
                }
            });
        }

        private void UpdateFolderDisplay(string message)
        {
            UI(() =>
                {
                    folderLabel.Content = "Folder: " + currentFolder.FolderPath + " (" + message + ")";
                    messagesLabel.Content = "Messages: 0/" + currentFolder.Messages.Count.ToString();
                    messagesProgressBar.Value = 0;
                    messagesProgressBar.Maximum = currentFolder.Messages.Count - 1;
                });
        }

        void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            if (currentFolder != null)
            {
                folderLabel.Content = "Folder: " + currentFolder.FolderPath;
                messagesProgressBar.Maximum = currentFolder.Messages.Count - 1;

                if (currentMessage != null)
                {
                    messagesProgressBar.Value = messageIndex;
                    messagesLabel.Content = String.Format("Messages: {0}/{1} - '{2} '", messageIndex, currentFolder.Messages.Count, currentMessage.Subject);
                }
            }
        }

        void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            btnRun.Content = "Run";
            btnRun.IsEnabled = true;

            Error("Skipped (already existed): " + numSkipped.ToString());
            Error("Uploaded: " + numUploaded.ToString());
            Error("Copied from destination folder (quick): " + numCopied);
            Error("Fixed (re-uploaded): " + numFixed);
            Error("Skipped (because of errors): " + numSkippedOnError);
            Console.Beep(256, 500);

            if (e.Error != null)
                MessageBox.Show("Error: '" + e.Error.Message + "'", "Transfer Failed");
        }

        void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            Connect();

            Error("Starting...");
            numUploaded = 0;
            numSkipped = 0;
            numCopied = 0;
            numFixed = 0;
            numSkippedOnError = 0;
            destMessages.Clear();

            //srcClient._client.SelectFolder("[Gmail]/All Mail");

            //RFC822Size size = new RFC822Size();
            //Command command = new UidFetch(13, size);
            //srcClient._client.ProcessCommand(command);

            //string data;
            //data = srcClient._client.FetchBodyInChunks(11935, true);
            //data = srcClient._client.FetchBodyInChunks(11935, true);

            foreach (var srcFolder in srcClient.Folders)
            {
                try
                {
                    string folderPath = srcFolder.FolderPath;

                    var destFolder = destClient.GetFolder(folderPath);
                    if (destFolder == null)
                    {
                        destClient.CreateFolder(folderPath);
                        destFolder = destClient.GetFolder(folderPath);
                    }

                    Debug.Assert(destFolder != null);

                    CopyFolder(srcFolder, destFolder);
                }
                catch (Exception ex)
                {
                    Error("Skipping Folder '" + srcFolder.FolderPath + "'\r\n" + ex.ToString());
                }
            }

            Disconnect();
        }

        private static ImapClient GetClient(string user, string password, string traceID)
        {
            ImapClient client = new ImapClient("imap.gmail.com", 993, true);
            client._client.TraceID = traceID;
            client.IsDebug = true;

            if (!client.Connect())
                throw new Exception("Connection failed");

            client.LogIn(user, password);
            return client;
        }

        private void Connect()
        {
            try
            {
                srcClient.Folders = null;
                srcClient.Connect();
                srcClient.LogIn(Settings.Default.srcEmail, Password.Decrypt(Settings.Default.srcPassword));
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.InnerException.Message, "Error logging in to source account", MessageBoxButton.OK, MessageBoxImage.Error);
                throw;
            }
            try
            {
                destClient.Folders = null;
                destClient.Connect();
                destClient.LogIn(Settings.Default.destEmail, Password.Decrypt(Settings.Default.destPassword));
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.InnerException.Message, "Error logging in to destination account", MessageBoxButton.OK, MessageBoxImage.Error);
                throw;
            }

            srcClient.IsDebug = Settings.Default.srcDebug;
            destClient.IsDebug = Settings.Default.destDebug;

            //srcClient._client.EnableCompression();
            //destClient._client.EnableCompression();
        }

        private void Disconnect()
        {
            try
            {
                srcClient.LogOut();
            }
            catch (Exception ex)
            {
                Error("Error logging out source account: '" + ex.Message + "'");
            }
            try
            {
                destClient.LogOut();
            }
            catch (Exception ex)
            {
                Error("Error logging out destination account: '" + ex.Message + "'");
            }

            srcClient.Disconnect();
            destClient.Disconnect();
        }

        //private void EnumerateSourceFolders(FolderCollection folderCollection, string path = "")
        //{
        //    foreach (var folder in folderCollection)
        //    {
        //        var newpath = ((path == "") ? "" : path + "/") + folder.Name;
        //        Debug.WriteLine("Folder: " + folder.FolderPath + " - " + newpath);
        //        folder.GetSubFolders();
        //        EnumerateSourceFolders(folder.SubFolders, newpath);
        //    }
        //}

        private void CopyFolder(Folder srcFolder, Folder destFolder)
        {
            if (srcFolder.IsSelectable) srcFolder.Select();
            if (destFolder.IsSelectable) destFolder.Select();

            CopyMessages(srcFolder, destFolder);
            CopySubFolders(srcFolder, destFolder);
        }

        enum MessageStatus
        {
            None,
            Skipped,
            Uploaded,
            Copied,
            Fixing,
            Fixed,
            Error
        }

        private void CopyMessages(Folder srcFolder, Folder destFolder)
        {
            if (!srcFolder.IsSelectable || srcFolder.Messages == null) return;

            messageIndex = 0;
            messageCount = destFolder.Messages.Count;

            currentFolder = destFolder;
            UpdateFolderDisplay("Indexing messages already on destination");

            Hashtable destHash = new Hashtable(); // list of messages already on server
            foreach (Message destMessage in destFolder.Messages)
            {
                messageIndex++;
                currentMessage = destMessage;
                UpdateMessageDisplay();

                try
                {
                    if (destHash.Contains(destMessage.MessageId))
                    {
                        Error("Source Duplicate: [" + destFolder.FolderPath + "] " + destMessage.ToString());
                    }
                    else
                    {
                        destHash.Add(destMessage.MessageId, destMessage);
                        if (!destMessages.ContainsKey(destMessage.MessageId))
                        {
                            destMessages.Add(destMessage.MessageId, new MessageLocation(destFolder.FolderPath, destMessage.MessageUid));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Error("Error indexing (skipping message: " + destMessage.ToString() + "): '" + ex.ToString() + "'");
                }
            }

            messageIndex = 0;
            messageCount = srcFolder.Messages.Count;
            currentFolder = srcFolder;
            UpdateFolderDisplay("Copying");

            errorSrcMessages.Clear();
            foreach (var srcMessage in srcFolder.Messages)
            {
                CopyMessage(srcMessage, destFolder, destHash);
            }

            UpdateFolderDisplay("Retrying Errors");
            messageIndex = 0;
            messageCount = errorSrcMessages.Count;
            foreach (var srcMessage in errorSrcMessages)
            {
                CopyMessage(srcMessage, destFolder, destHash);
            }
        }

        private void CopyMessage(Message srcMessage, Folder destFolder, Hashtable destHash)
        {
            MessageStatus status = MessageStatus.None;

            Message destMessage = null;

            messageIndex++;

            try
            {
                RetryCode.Run(3, (thisTry) =>
                {
                    currentMessage = srcMessage;
                    UpdateMessageDisplay();
                    //worker.ReportProgress(messageIndex * 100 / srcFolder.Messages.Count);

                    Debug.WriteLine("Message: '" + srcMessage.Subject + "'");

                    if (destHash.Contains(srcMessage.MessageId))
                    {
                        // Message is already on destination
                        Debug.WriteLine("Exists on destination");
                        destMessage = (Message)destHash[srcMessage.MessageId];
                        Debug.Assert(status != MessageStatus.Fixing);
                        status = MessageStatus.Skipped;
                        numSkipped++;
                    }
                    else
                    {
                        if (destMessages.ContainsKey(srcMessage.MessageId))
                        {
                            // message exists in another folder on destination
                            // just copy it instead of uploading it
                            var srcLocation = destMessages[srcMessage.MessageId];
                            Debug.WriteLine("Copying message from " + srcLocation.folderPath);
                            destFolder.CopyMessageFromFolder(srcLocation.uid, srcLocation.folderPath, out destMessage);
                            Debug.Assert(status != MessageStatus.Fixing);
                            status = MessageStatus.Copied;
                            numCopied++;
                        }
                        else
                        {
                            Debug.WriteLine("Downloading source data...");
                            var data = srcMessage.OriginalData;

                            Debug.WriteLine("Uploading Message...");
                            destMessage = UploadMessage(destFolder, srcMessage);
                            if (status != MessageStatus.Fixing)
                            {
                                status = MessageStatus.Uploaded;
                                numUploaded++;
                            }
                        }
                    }

                    if (Settings.Default.verify)
                    {
                        string srcBodyStruct = srcClient._client.SendAndReceive("UID FETCH " + srcMessage.MessageUid + " (BODYSTRUCTURE)\r\n");
                        srcBodyStruct = Regex.Match(srcBodyStruct, "BODYSTRUCTURE.*(?=\r\n)").Value;
                        Debug.WriteLine("Source bodystruct: " + srcBodyStruct);

                        string destBodyStruct = destClient._client.SendAndReceive("UID FETCH " + destMessage.MessageUid + " (BODYSTRUCTURE)\r\n");
                        destBodyStruct = Regex.Match(destBodyStruct, "BODYSTRUCTURE.*(?=\r\n)").Value;
                        Debug.WriteLine("Dest   bodystruct: " + destBodyStruct);

                        Debug.WriteLineIf(srcBodyStruct == destBodyStruct, "BODYSTRUCTURE match!!!");

                        if (srcBodyStruct != destBodyStruct)
                        {

                            Debug.WriteLine("BODYSTRUCTURE doesn't match. Checking Data...");
                            Debug.WriteLine("Reading source data...");
                            var srcData = srcMessage.OriginalData;
                            Debug.WriteLine("Reading destination data...");
                            var destData = destMessage.OriginalData;

                            Debug.WriteLine("Fetched size s:+ " + srcData.Length + " d:" + destData.Length);

                            int srcLines = srcData.Count((b) => { return b == 13; });
                            int destLines = destData.Count((b) => { return b == 13; });

                            Debug.WriteLine("Size diff:" + (destMessage.Size - srcMessage.Size).ToString() + " Lines s:" + srcLines.ToString() + " d:" + destLines.ToString());

                            srcMessage.UnloadMessageBody();
                            destMessage.UnloadMessageBody();

                            if (!srcData.SequenceEqual(destData))
                            {
                                if (thisTry > 1)
                                {
                                    Debug.WriteLine("Data doesn't match.  Deleting message.");
                                    status = MessageStatus.Fixing;
                                    switch (status)
                                    {
                                        case MessageStatus.Skipped:
                                            numSkipped--;
                                            break;
                                        case MessageStatus.Uploaded:
                                            numUploaded--;
                                            break;
                                        case MessageStatus.Copied:
                                            numCopied--;
                                            break;
                                    }
                                    destFolder.DeleteMessage(destMessage);
                                    destMessages.Remove(srcMessage.MessageId);
                                    destHash.Remove(srcMessage.MessageId);
                                }

                                throw new Exception("Data didn't match for source and destination"
                                    + "\r\n\tSource: " + srcMessage.ToString()
                                    + "\r\n\tDestination: " + destMessage.ToString());
                            }
                            Debug.WriteLine("But data matches");
                        }

                        if (Settings.Default.syncFlags)
                        {
                            srcMessage.ProcessFlags();
                            Message.SetFlag(destClient._client, srcMessage.Flags.ToString(), destMessage.MessageUid);
                        }
                    }

                    if (status == MessageStatus.Fixing)
                    {
                        status = MessageStatus.Fixed;
                        numFixed++;
                    }
                });
            }
            catch (Exception ex)
            {
                errorSrcMessages.Add(srcMessage);
                Error("Error (skipping message: " + srcMessage.ToString() + "): '" + ex.ToString() + "'");
                numSkippedOnError++;
            }
            finally
            {
                if (destMessage != null) destMessage.UnloadMessageBody();
                if (srcMessage != null) srcMessage.UnloadMessageBody();
            }
        }

        private Message UploadMessage(Folder destFolder, Message srcMessage)
        {
            Message destMessage;

            destMessage = destFolder.AppendMessage(srcMessage);

            if (destMessage != null)
            {
                destMessages.Add(srcMessage.MessageId, new MessageLocation(destFolder.FolderPath, destMessage.MessageUid));
            }

            if (destMessage.InternalDate != srcMessage.InternalDate) throw new Exception("Internal date was not set properly s:" + srcMessage.InternalDate + " d:" + destMessage.InternalDate);

            return destMessage;
        }

        private void CopySubFolders(Folder srcFolder, Folder destFolder)
        {
            foreach (var srcSubFolder in srcFolder.SubFolders)
            {
                string name = srcSubFolder.Name;

                if (!destFolder.SubFolders.Exists((f => f.Name == name)))
                {
                    destFolder.CreateFolder(name);
                }

                CopyFolder(srcSubFolder, destFolder.SubFolders[name]);

            }
        }

        private bool SavePasswords
        {
            get
            {
                return savePasswordsCheckBox.IsChecked.GetValueOrDefault(false);
            }
        }

        private void srcPassword_PasswordChanged(object sender, RoutedEventArgs e)
        {
            string password = "";
            if (SavePasswords)
            {
                password = Password.Encrypt(srcPassword.Password);
            }
            Settings.Default.srcPassword = password;
            Settings.Default.Save();
        }

        private void destPassword_PasswordChanged(object sender, RoutedEventArgs e)
        {
            string password = "";
            if (SavePasswords)
            {
                password = Password.Encrypt(destPassword.Password);
            }
            Settings.Default.destPassword = password;
            Settings.Default.Save();
        }

        private void savePasswordsCheckBox_Click(object sender, RoutedEventArgs e)
        {
            string srcPwd = "";
            string destPwd = "";

            if (savePasswordsCheckBox.IsChecked.GetValueOrDefault(false))
            {
                srcPwd = Password.Encrypt(srcPassword.Password);
                destPwd = Password.Encrypt(destPassword.Password);
            }

            Settings.Default.srcPassword = srcPwd;
            Settings.Default.destPassword = destPwd;

            Settings.Default.Save();
        }

        private void destDebugCheckBox_Click(object sender, RoutedEventArgs e)
        {
            if (destClient != null)
            {
                destClient.IsDebug = destDebugCheckBox.IsChecked.GetValueOrDefault(false);
            }
        }

        private void srcDebugCheckBox_Click(object sender, RoutedEventArgs e)
        {
            if (srcClient != null)
            {
                srcClient.IsDebug = srcDebugCheckBox.IsChecked.GetValueOrDefault(false);
            }
        }
    }
}
